package com.bigknow.appstore.server.rpc;

import static com.bigknow.org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static com.bigknow.org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static com.bigknow.org.objectweb.asm.Opcodes.ACC_SUPER;
import static com.bigknow.org.objectweb.asm.Opcodes.ACONST_NULL;
import static com.bigknow.org.objectweb.asm.Opcodes.ALOAD;
import static com.bigknow.org.objectweb.asm.Opcodes.ARETURN;
import static com.bigknow.org.objectweb.asm.Opcodes.ASTORE;
import static com.bigknow.org.objectweb.asm.Opcodes.ATHROW;
import static com.bigknow.org.objectweb.asm.Opcodes.CHECKCAST;
import static com.bigknow.org.objectweb.asm.Opcodes.DLOAD;
import static com.bigknow.org.objectweb.asm.Opcodes.DRETURN;
import static com.bigknow.org.objectweb.asm.Opcodes.DSTORE;
import static com.bigknow.org.objectweb.asm.Opcodes.DUP;
import static com.bigknow.org.objectweb.asm.Opcodes.FLOAD;
import static com.bigknow.org.objectweb.asm.Opcodes.FRETURN;
import static com.bigknow.org.objectweb.asm.Opcodes.FSTORE;
import static com.bigknow.org.objectweb.asm.Opcodes.GETFIELD;
import static com.bigknow.org.objectweb.asm.Opcodes.IFEQ;
import static com.bigknow.org.objectweb.asm.Opcodes.ILOAD;
import static com.bigknow.org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static com.bigknow.org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static com.bigknow.org.objectweb.asm.Opcodes.INVOKESTATIC;
import static com.bigknow.org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static com.bigknow.org.objectweb.asm.Opcodes.IRETURN;
import static com.bigknow.org.objectweb.asm.Opcodes.ISTORE;
import static com.bigknow.org.objectweb.asm.Opcodes.LLOAD;
import static com.bigknow.org.objectweb.asm.Opcodes.LRETURN;
import static com.bigknow.org.objectweb.asm.Opcodes.LSTORE;
import static com.bigknow.org.objectweb.asm.Opcodes.NEW;
import static com.bigknow.org.objectweb.asm.Opcodes.PUTFIELD;
import static com.bigknow.org.objectweb.asm.Opcodes.RETURN;
import static com.bigknow.org.objectweb.asm.Opcodes.V1_5;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.ssl.SslHandler;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import com.bigknow.org.objectweb.asm.ClassWriter;
import com.bigknow.org.objectweb.asm.Label;
import com.bigknow.org.objectweb.asm.MethodVisitor;
import com.bigknow.org.objectweb.asm.Type;

import sun.security.x509.X500Name;

import com.bigknow.appstore.common.rpc.RPCHandler;
import com.bigknow.appstore.common.rpc.RPCProxy;
import com.bigknow.appstore.common.rpc.RPCRequest;
import com.bigknow.appstore.common.rpc.RPCResponse;
import com.bigknow.appstore.common.rpc.RPCUtil;


@SuppressWarnings("restriction")
public class RPCServer implements MuxActionHandler {

    private static Map<String, RPCSession> sessions = new ConcurrentHashMap<String, RPCSession>();

    private ExecutorService threadPool = Executors.newCachedThreadPool();

    private Map<ChannelHandlerContext, RPCSession> ctx2Sessions = new ConcurrentHashMap<ChannelHandlerContext, RPCSession>();

    private static Map<Class<?>, Class<?>> requestProxyMap = new ConcurrentHashMap<Class<?>, Class<?>>();

    private static Map<String, Map<String, RPCHandler>> rpcHandlerMap = new ConcurrentHashMap<String, Map<String, RPCHandler>>();

    private static List<ConnectionListener> connectionListeners = new Vector<>();

    @Override
    public void init(ChannelHandlerContext ctx) {
        try {
            X509Certificate cert = (X509Certificate) ctx.pipeline()
                    .get(SslHandler.class).engine().getSession()
                    .getPeerCertificates()[0];
            String name = cert.getSubjectX500Principal().getName();
            X500Name x500Name = new X500Name(name);
            String id = x500Name.getDNQualifier();
            if (sessions.containsKey(id) && !id.equals("appstore_DESIGNER")) {
                //TODO 警告
                ctx.close();
                return;
            }
            RPCSession session = new RPCSession(this, id, ctx);
            sessions.put(id, session);
            ctx2Sessions.put(ctx, session);
            connectionListeners.forEach(l -> l.notifyConnect(id));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean handle(ChannelHandlerContext ctx, ByteBuf msg) {
        RPCSession session = ctx2Sessions.get(ctx);
        return session.handle(msg);
    }

    @Override
    public void notifyDisconnect(ChannelHandlerContext ctx) {
        RPCSession session = ctx2Sessions.remove(ctx);
        if (session != null) {
            String id = session.getSysId();
            sessions.remove(id);
            connectionListeners.forEach(l -> l.notifyConnect(id));
        }
    }

    public ExecutorService getThreadPool() {
        return threadPool;
    }

    public static RPCSession getSession(String id) {
        return sessions.get(id);
    }

    public static <T> void registerRPCInterface(Class<T> interfaceClass, T impl) {
        if (!interfaceClass.isInterface()) {
            throw new RuntimeException(interfaceClass.getName() + " 不是接口");
        }
        Map<String, RPCHandler> methodHandlers = new HashMap<String, RPCHandler>();
        for (Method method : interfaceClass.getDeclaredMethods()) {
            createRequestClass(method, RPCUtil.getImplClassName(interfaceClass));
            createResponseClass(method,
                    RPCUtil.getImplClassName(interfaceClass));
            String requestClass = RPCUtil.getMethodRequestName(method,
                    RPCUtil.getImplClassName(interfaceClass)).replace('.', '/');
            String className = RPCUtil
                    .getProxyClassName(interfaceClass, method)
                    .replace('.', '/');
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, className, null,
                    Type.getInternalName(RPCHandler.class), null);

            MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V",
                    null, null);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL,
                    Type.getInternalName(RPCHandler.class), "<init>", "()V");
            mv.visitInsn(RETURN);
            mv.visitMaxs(1, 1);
            mv.visitEnd();

            mv = cw.visitMethod(
                    ACC_PUBLIC,
                    "handler",
                    "(Lcom/bigknow/appstore/common/rpc/RPCRequest;)Lcom/bigknow/appstore/common/rpc/RPCResponse;",
                    null, null);
            Class<?>[] params = method.getParameterTypes();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, className, "impl",
                    Type.getDescriptor(Object.class));
            mv.visitTypeInsn(CHECKCAST, Type.getInternalName(interfaceClass));
            for (int i = 0; i < params.length; i++) {
                mv.visitVarInsn(ALOAD, 1);
                mv.visitTypeInsn(CHECKCAST, requestClass);
                Class<?> param = params[i];
                mv.visitMethodInsn(INVOKEVIRTUAL, requestClass, "getArg" + i,
                        "()" + Type.getDescriptor(param));
            }
            mv.visitMethodInsn(INVOKEINTERFACE,
                    Type.getInternalName(interfaceClass), method.getName(),
                    Type.getMethodDescriptor(method));
            Class<?> returnType = method.getReturnType();
            if (returnType == void.class) {
                mv.visitInsn(ACONST_NULL);
                mv.visitInsn(ARETURN);
                mv.visitMaxs(2, 1);
            } else {
                String responseInnerName = RPCUtil.getMethodResponseName(
                        method, RPCUtil.getImplClassName(interfaceClass))
                        .replace('.', '/');
                int len = addStoreCode(mv, returnType, 2);
                mv.visitTypeInsn(NEW, responseInnerName);
                mv.visitInsn(DUP);
                mv.visitMethodInsn(INVOKESPECIAL, responseInnerName, "<init>",
                        "()V");
                int responseVarIndex = 2 + len;
                mv.visitVarInsn(ASTORE, responseVarIndex);
                mv.visitVarInsn(ALOAD, responseVarIndex);
                addLoadCode(mv, returnType, 2);
                mv.visitMethodInsn(INVOKEVIRTUAL, responseInnerName,
                        "setReturnObj", "(" + Type.getDescriptor(returnType)
                                + ")V");
                mv.visitVarInsn(ALOAD, responseVarIndex);
                mv.visitInsn(ARETURN);
                mv.visitMaxs(4, 3);
            }
            mv.visitEnd();
            cw.visitEnd();
            byte[] code = cw.toByteArray();

            try {
                Class<?> clz = RPCUtil.getClassLoader()
                        .defineClass(
                                RPCUtil.getProxyClassName(interfaceClass,
                                        method), code);
                RPCHandler methodHandler = (RPCHandler) clz.newInstance();
                methodHandler.setImpl(impl);
                methodHandlers.put(method.getName(), methodHandler);
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        }
        RPCUtil.parseInterfaceClass(interfaceClass);
        rpcHandlerMap.put(interfaceClass.getName(), methodHandlers);
    }

    @SuppressWarnings("unchecked")
    public static <T> T getProxy(Class<T> interfaceClass, String sysId) {
        RPCProxy impl = null;
        try {
            Class<?> implClass = requestProxyMap.get(interfaceClass);
            if (implClass != null) {
                impl = (RPCProxy) implClass.newInstance();
                impl.setSysId(sysId);
                return (T) impl;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        String className = RPCUtil.getImplClassName(interfaceClass);
        String internName = interfaceClass.getName().replace('.', '/');

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, className.replace('.', '/'),
                null, Type.getInternalName(RPCProxy.class),
                new String[]{internName});

        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null,
                null);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, "com/bigknow/appstore/common/rpc/RPCProxy",
                "<init>", "()V");
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        Method[] methods = interfaceClass.getMethods();
        for (Method method : methods) {
            createRequestClass(method, className);
            createResponseClass(method, className);
            createMethod(cw, method, className);
        }

        cw.visitEnd();
        RPCUtil.parseInterfaceClass(interfaceClass);

        byte[] code = cw.toByteArray();
        Class<T> implClass = (Class<T>) RPCUtil.getClassLoader().defineClass(
                className, code);
        try {
            requestProxyMap.put(interfaceClass, implClass);
            impl = (RPCProxy) implClass.newInstance();
            impl.setSysId(sysId);
            return (T) impl;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static void createRequestClass(Method method, String className) {
        Class<?>[] parms = method.getParameterTypes();
        if (parms.length <= 0)
            return;
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        String requestClassName = RPCUtil.getMethodRequestName(method,
                className).replace('.', '/');
        cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, requestClassName, null,
                "com/bigknow/appstore/common/rpc/RPCRequest", new String[]{});
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null,
                null);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, "com/bigknow/appstore/common/rpc/RPCRequest",
                "<init>", "()V");
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        java.lang.reflect.Type[] params = method.getGenericParameterTypes();
        Class<?>[] paramClasses = method.getParameterTypes();
        for (int i = 0; i < params.length; i++) {
            java.lang.reflect.Type param = params[i];
            cw.visitField(ACC_PRIVATE, "arg" + i,
                    Type.getDescriptor(paramClasses[i]),
                    getSignature(param, paramClasses[i]), null);
        }

        for (int i = 0; i < params.length; i++) {
            java.lang.reflect.Type param = params[i];
            String descriptor = Type.getDescriptor(paramClasses[i]);
            String signature = getSignature(param, paramClasses[i]);
            mv = cw.visitMethod(ACC_PUBLIC, "setArg" + i, "(" + descriptor
                    + ")V", "(" + signature + ")V", null);
            mv.visitVarInsn(ALOAD, 0);
            addLoadCode(mv, paramClasses[i], 1);
            mv.visitFieldInsn(PUTFIELD, requestClassName, "arg" + i, descriptor);
            mv.visitInsn(RETURN);
            mv.visitMaxs(2, 1);
            mv.visitEnd();

            mv = cw.visitMethod(ACC_PUBLIC, "getArg" + i, "()" + descriptor,
                    "()" + signature, null);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, requestClassName, "arg" + i, descriptor);
            addReturnCode(mv, paramClasses[i]);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }
        cw.visitEnd();
        byte[] code = cw.toByteArray();

        RPCUtil.getClassLoader().defineClass(
                RPCUtil.getMethodRequestName(method, className), code);
    }

    private static String getSignature(java.lang.reflect.Type t, Class<?> clz) {
        if (t instanceof ParameterizedType) {
            ParameterizedType type = (ParameterizedType) t;
            String desc = Type.getDescriptor((Class<?>) type.getRawType());
            StringBuilder buf = new StringBuilder(desc.substring(0,
                    desc.length() - 1));
            buf.append('<');
            for (java.lang.reflect.Type actualType : type
                    .getActualTypeArguments()) {
                buf.append(getSignature(actualType, clz));
            }
            buf.append(">;");
            return buf.toString();
        } else if (t instanceof GenericArrayType) {
            return Type.getDescriptor(clz);
        } else if (t instanceof Class) {
            return Type.getDescriptor((Class<?>) t);
        } else {
            throw new UnsupportedOperationException("不支持的泛型类型(" + t + ")");
        }
    }

    private static void createResponseClass(Method method, String className) {
        ClassWriter cw;
        MethodVisitor mv;
        byte[] code;
        Class<?> returnClz = method.getReturnType();
        java.lang.reflect.Type returnType = method.getGenericReturnType();
        if (returnClz != void.class) {
            cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            String responseClassName = RPCUtil.getMethodResponseName(method,
                    className).replace('.', '/');
            cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, responseClassName, null,
                    "com/bigknow/appstore/common/rpc/RPCResponse", new String[]{});
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL,
                    "com/bigknow/appstore/common/rpc/RPCResponse", "<init>", "()V");
            mv.visitInsn(RETURN);
            mv.visitMaxs(1, 1);
            mv.visitEnd();

            String descriptor = Type.getDescriptor(returnClz);
            String signature = getSignature(returnType, returnClz);
            cw.visitField(ACC_PRIVATE, "returnObj", descriptor, signature, null);

            mv = cw.visitMethod(ACC_PUBLIC, "setReturnObj", "(" + descriptor
                    + ")V", "(" + signature + ")V", null);
            mv.visitVarInsn(ALOAD, 0);
            addLoadCode(mv, returnClz, 1);
            mv.visitFieldInsn(PUTFIELD, responseClassName, "returnObj",
                    descriptor);
            mv.visitInsn(RETURN);
            mv.visitMaxs(2, 1);
            mv.visitEnd();

            mv = cw.visitMethod(ACC_PUBLIC, "getReturnObj", "()" + descriptor,
                    "()" + signature, null);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, responseClassName, "returnObj",
                    descriptor);
            addReturnCode(mv, returnClz);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
            cw.visitEnd();
            code = cw.toByteArray();

            RPCUtil.getClassLoader().defineClass(
                    RPCUtil.getMethodResponseName(method, className), code);
        }
    }

    private static int addLoadCode(MethodVisitor mv, Class<?> param, int index) {
        if (param.isPrimitive()) {
            if (param == float.class) {
                mv.visitVarInsn(FLOAD, index);
            } else if (param == double.class) {
                mv.visitVarInsn(DLOAD, index);
                return 2;
            } else if (param == long.class) {
                mv.visitVarInsn(LLOAD, index);
                return 2;
            } else {
                mv.visitVarInsn(ILOAD, index);
            }
        } else {
            mv.visitVarInsn(ALOAD, index);
        }
        return 1;
    }

    private static int addStoreCode(MethodVisitor mv, Class<?> param, int index) {
        if (param.isPrimitive()) {
            if (param == float.class) {
                mv.visitVarInsn(FSTORE, index);
            } else if (param == double.class) {
                mv.visitVarInsn(DSTORE, index);
                return 2;
            } else if (param == long.class) {
                mv.visitVarInsn(LSTORE, index);
                return 2;
            } else {
                mv.visitVarInsn(ISTORE, index);
            }
        } else {
            mv.visitVarInsn(ASTORE, index);
        }
        return 1;
    }

    private static void addReturnCode(MethodVisitor mv, Class<?> param) {
        if (param.isPrimitive()) {
            if (param == float.class) {
                mv.visitInsn(FRETURN);
            } else if (param == double.class) {
                mv.visitInsn(DRETURN);
            } else if (param == long.class) {
                mv.visitInsn(LRETURN);
            } else {
                mv.visitInsn(IRETURN);
            }
        } else {
            mv.visitInsn(ARETURN);
        }
    }

    private static void createMethod(ClassWriter cw, Method method,
                                     String className) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, method.getName(),
                Type.getMethodDescriptor(method), null, null);
        mv.visitCode();
        String requestClzName = RPCUtil.getMethodRequestName(method, className)
                .replace('.', '/');
        mv.visitTypeInsn(NEW, requestClzName);
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESPECIAL, requestClzName, "<init>", "()V");

        Class<?>[] params = method.getParameterTypes();
        int paramsLength = 0;
        for (Class<?> param : params) {
            if (param == long.class || param == double.class) {
                paramsLength += 2;
            } else {
                paramsLength++;
            }
        }
        int requestVarIndex = paramsLength + 1;
        mv.visitVarInsn(ASTORE, requestVarIndex);
        int index = 1;
        for (int i = 0; i < params.length; i++) {
            mv.visitVarInsn(ALOAD, requestVarIndex);
            index += addLoadCode(mv, params[i], index);
            mv.visitMethodInsn(INVOKEVIRTUAL, requestClzName, "setArg" + i, "("
                    + Type.getDescriptor(params[i]) + ")V");
        }

        mv.visitVarInsn(ALOAD, requestVarIndex);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className.replace('.', '/'), "sysId",
                Type.getDescriptor(String.class));
        mv.visitMethodInsn(INVOKEVIRTUAL, requestClzName, "setSysId",
                "(Ljava/lang/String;)V");

        mv.visitVarInsn(ALOAD, requestVarIndex);
        mv.visitLdcInsn(className.substring(0, className.length() - 5));
        mv.visitMethodInsn(INVOKEVIRTUAL, requestClzName, "setInterfaceName",
                "(Ljava/lang/String;)V");

        mv.visitVarInsn(ALOAD, requestVarIndex);
        mv.visitLdcInsn(method.getName());
        mv.visitMethodInsn(INVOKEVIRTUAL, requestClzName, "setMethodName",
                "(Ljava/lang/String;)V");

        mv.visitVarInsn(ALOAD, requestVarIndex);
        mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(RPCServer.class),
                "invoke",
                "(Lcom/bigknow/appstore/common/rpc/RPCRequest;)Lcom/bigknow/appstore/common/rpc/RPCResponse;");
        int responseVarIndex = paramsLength + 2;
        mv.visitVarInsn(ASTORE, responseVarIndex);
        mv.visitVarInsn(ALOAD, responseVarIndex);
        mv.visitMethodInsn(INVOKEVIRTUAL,
                Type.getInternalName(RPCResponse.class), "isError", "()Z");
        Label label = new Label();
        mv.visitJumpInsn(IFEQ, label);

        mv.visitTypeInsn(NEW, Type.getInternalName(RuntimeException.class));
        mv.visitInsn(DUP);
        mv.visitVarInsn(ALOAD, responseVarIndex);
        mv.visitMethodInsn(INVOKEVIRTUAL,
                Type.getInternalName(RPCResponse.class), "getErrorMsg",
                "()Ljava/lang/String;");
        mv.visitMethodInsn(INVOKESPECIAL,
                Type.getInternalName(RuntimeException.class), "<init>",
                "(Ljava/lang/String;)V");
        mv.visitInsn(ATHROW);

        mv.visitLabel(label);
        Class<?> returnType = method.getReturnType();
        String responseClzName = RPCUtil.getMethodResponseName(method,
                className).replace('.', '/');
        if (returnType != void.class) {
            mv.visitVarInsn(ALOAD, responseVarIndex);
            mv.visitTypeInsn(CHECKCAST, responseClzName);
            mv.visitMethodInsn(INVOKEVIRTUAL, responseClzName, "getReturnObj",
                    "()" + Type.getDescriptor(returnType));
            addReturnCode(mv, returnType);
        } else {
            mv.visitInsn(RETURN);
        }
        mv.visitMaxs(paramsLength + 3, params.length + 2);
        mv.visitEnd();
    }

    public static boolean isConnected(String id) {
        return sessions.containsKey(id);
    }

    public static RPCResponse invoke(RPCRequest req) {
        RPCSession session = sessions.get(req.getSysId());
        if (session == null) {
            throw new RuntimeException("应用未连接");
        }
        return session.invoke(req);
    }

    public Map<String, Map<String, RPCHandler>> getRPCHandlerMap() {
        return rpcHandlerMap;
    }

    public static void registerConnectionListener(ConnectionListener listener) {
        connectionListeners.add(listener);
    }

    public static void unregisterConnectionListener(ConnectionListener listener) {
        connectionListeners.remove(listener);
    }
}
